import { RedisClientType, createClient } from 'redis';
let redisMap: { [key: number]: RedisClientType | null } = {};
async function useRedis(databsae: number = 0): Promise<RedisClientType> {
    if (redisMap[databsae]) {
        return redisMap[databsae]!;
    }
    const redisUrl = process.env.REDIS_URL || ``;
    const redis = createClient({
        url: redisUrl,
        database: databsae
    }) as RedisClientType;
    await redis.connect()
    return redis;
}

async function set<T = any>(key: RedisToken<T>, val: T, database: number = 0) {
    return await redis(async client => {
        if (typeof val === 'string') {
            return await client.set(key, val).catch(e => {
                redisMap[database] = null;
                throw e;
            });
        }
        return await client.set(key, JSON.stringify(val)).catch(e => {
            redisMap[database] = null;
            throw e;
        });
    })
}

async function setExp<T = any>(key: RedisToken<T>, val: T, exip: number = 10, database: number = 0) {
    return await redis(async client => {
        if (typeof val === 'string') {
            await client.set(key, val).catch(e => {
                redisMap[database] = null;
                throw e;
            });
        } else {
            await client.set(key, JSON.stringify(val)).catch(e => {
                redisMap[database] = null;
                throw e;
            });
        }
        await client.expire(key, exip).catch(e => {
            redisMap[database] = null;
            throw e;
        });
    })

}
async function del(key: string, database: number = 0) {
    return await redis(async client => {
        return await client.del(key).catch(e => {
            redisMap[database] = null;
            throw e;
        });;
    })
}
function safeParseJson(str?: string | null) {
    if (!str) return;
    try {
        return JSON.parse(str);
    } catch (e) {
        return str;
    }
}
export type RedisToken<T> = string | (string & { __type?: T })
async function get<T = any>(key: RedisToken<T>, database: number = 0) {
    return await redis<T>(async client => {
        const str = await client.get(key).catch(e => {
            redisMap[database] = null;
            throw e;
        });
        const res = safeParseJson(str);
        return res;
    })
}
async function gets<T = any>(key: RedisToken<T>, database: number = 0) {
    return await redis(async client => {
        const keys = await client.keys(key)
        if (!keys) return []
        const res = Promise.all(keys.map(async key => {
            const val = await client.get(key).catch(e => {
                redisMap[database] = null;
                throw e;
            });
            return { key, val: safeParseJson(val) as T }
        }))
        return res;
    })
}
async function lock(key: string, value: string = `1`, database: number = 0) {
    return await redis(async client => {
        const res = await client.set(key, value)
        const success = res === 'OK'
        return success;
    })
}

async function autoLock(key: string, value: string, expire: number, database: number = 0) {
    return await redis(async client => {
        const res = await client.set(key, value).catch(e => {
            redisMap[database] = null;
            throw e;
        })
        const success = res === 'OK'
        await client.expire(key, expire).catch(e => {
            redisMap[database] = null;
            throw e;
        })
        return success;
    })
}

async function unLock(key: string, database: number = 0) {
    await redis(async client => {
        await client.del(key)
    })
    return true;
}

async function waitFor(key: string, database: number = 0) {
    await redis(async client => {
        let lock = await client.get(key)
        while (lock) {
            await new Promise<void>((resolve, reject) => {
                setTimeout(() => resolve(), 1000)
            });
            lock = await client.get(key)
        }
    })
    return get(key, database)
}

async function waitUnLock(key: string, database: number = 0) {
    return redis(async client => {
        let lock = await client.get(key)
        while (lock) {
            await new Promise<void>((resolve, reject) => {
                setTimeout(() => resolve(), 1000)
            });
            lock = await client.get(key)
        }
    })
}

async function zAdd(key: string, score: number, value: string | Buffer, database: number = 0) {
    return redis(async c => {
        return await c.zAdd(key, { score: score, value: value })
    })
}
export async function sAdd(key: string, value: string | Buffer, database: number = 0) {
    return redis(async c => {
        return await c.sAdd(key, value)
    }, database)
}
export async function sAdds(key: string, value: (string | Buffer)[], database: number = 0) {
    return redis(async c => {
        return await c.sAdd(key, value)
    }, database)
}
async function zRem(key: string, value: string | Buffer, database: number = 0) {
    return redis(async c => {
        return await c.zRem(key, value)
    })
}
async function redis<T = any>(handler: (client: RedisClientType) => Promise<T>, database: number = 0) {
    const redisUrl = process.env.REDIS_URL || ``;
    const redis = createClient({
        url: redisUrl,
        database: database
    }) as RedisClientType;
    await redis.connect()
    return handler(redis).catch(e => {
        throw e;
    }).finally(() => {
        redis.disconnect()
    })
}
export {
    del, get, gets, lock, set, setExp, unLock,
    useRedis, waitFor, redis,
    waitUnLock, autoLock, zAdd, zRem
};

